﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using DevExpress.Xpf.Core;
using Microsoft.Win32;
using MoreLinq;
using StringExtensions;
using static System.Math;

namespace CableInitForceGen
{
    public partial class MainWindow : ThemedWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private string _mctFilePath;
        private readonly MctModel _mctModel = new MctModel();

        private void ButtonEditMctFile_OnButtonClick(object sender, RoutedEventArgs e)
        {
            var ofd = new OpenFileDialog {DefaultExt = ".mct", Filter = "Midas Civil MCT file (.mct)|*.mct"};
            if (ofd.ShowDialog(this) != true)
                return;

            _mctFilePath = ofd.FileName;
            Debug.Assert(File.Exists(_mctFilePath));
            ButtonEditMctFile.Text = _mctFilePath;

            //
            _mctModel.ReadMctFileData(_mctFilePath);

            UpdateControls();
        }

        private void UpdateControls()
        {
            Debug.Assert(_mctModel.GroupDict != null);
            var groupNames = _mctModel.GroupDict.Select(g => g.Key).ToList();

            ComboBoxCableGroupName.ItemsSource = groupNames;
            ComboBoxCableGroupName.SelectedIndex = groupNames.FindIndex(name => name == "斜拉索");

            ListBoxConstrainNodeDx.ItemsSource = groupNames;
            groupNames.Where(groupName => groupName.EndsWith("DX"))
                .ForEach(groupName => ListBoxConstrainNodeDx.SelectedItems.Add(groupName));

            ListBoxConstrainNodeDz.ItemsSource = groupNames;
            groupNames.Where(groupName => groupName.EndsWith("DZ"))
                .ForEach(groupName => ListBoxConstrainNodeDz.SelectedItems.Add(groupName));

            ListBoxConstrainCableTension.ItemsSource = groupNames;
            groupNames.Where(groupName => groupName.EndsWith("T"))
                .ForEach(groupName => ListBoxConstrainCableTension.SelectedItems.Add(groupName));
        }

        private static (Node UpperNode, Node LowerNode) GetUpperLowerNode(Elem el,
            IReadOnlyDictionary<int, Node> nodeDict)
        {
            var n1 = nodeDict[el.NodeIds[0]];
            var n2 = nodeDict[el.NodeIds[1]];
            return n1.Z > n2.Z ? (n1, n2) : (n2, n1);
        }

        private void ButtonGenerateMct_OnClick(object sender, RoutedEventArgs e)
        {
            var cableGroupName = (string) ComboBoxCableGroupName.SelectedItem; // 单侧分组拉索数量
            var constrainDxNodeGroupNames = ListBoxConstrainNodeDx.SelectedItems.Cast<string>();
            var constrainDzNodeGroupNames = ListBoxConstrainNodeDz.SelectedItems.Cast<string>();
            var constrainTensionCableGroupNames = ListBoxConstrainCableTension.SelectedItems.Cast<string>();

            var cableCount = (int) SpinEditCableCountPerGroup.Value;
            var pretension = (double) SpinEditPretension.Value;
            var minDx = (double) SpinEditMinDx.Value;
            var maxDx = (double) SpinEditMaxDx.Value;
            var minDz = (double) SpinEditMinDz.Value;
            var maxDz = (double) SpinEditMaxDz.Value;

            var minTensionRatio = (double) SpinEditMinTensionRatio.Value / 100D; // 百分比
            var maxTensionRatio = (double) SpinEditMaxTensionRatio.Value / 100D; //百分比

            var selfWeight = (double) SpinEditSelfWeight.Value;
            var deadLoad2 = (double) SpinEditDeadLoad2.Value;
            var sectionLength = (double) SpinEditSectionLength.Value;

            var loadCombName = TextBoxCableUnitForceLoadCombName.Text;
            var deadLoadName1 = TextBoxDeadLoadName1.Text;
            var deadLoadName2 = TextBoxDeadLoadName2.Text;

            var cableList = _mctModel.GroupDict[cableGroupName].ElemIds
                .Select(id => _mctModel.AllElemDict[id])
                .OrderBy(el => GetUpperLowerNode(el, _mctModel.AllNodeDict).LowerNode.X).ToList();

            var constrainDxNodeIds = constrainDxNodeGroupNames
                .SelectMany(groupName => _mctModel.GroupDict[groupName].NodeIds)
                .OrderBy(id => id).Distinct().ToList();
            var constrainDzNodeIds = constrainDzNodeGroupNames
                .SelectMany(groupName => _mctModel.GroupDict[groupName].NodeIds)
                .OrderBy(id => id).Distinct().ToList();
            var constrainTensionElemIds = constrainTensionCableGroupNames
                .SelectMany(groupName => _mctModel.GroupDict[groupName].ElemIds)
                .OrderBy(id => id).Distinct().ToList();

            //
            // group cable
            //
            var cableCountPerGroup = 2 * 2 * cableCount; //两侧对称，双索面，每组相邻两对索
            Debug.Assert(cableList.Count % cableCountPerGroup == 0);
            var cableGroups = MoreEnumerable.Sequence(0, cableList.Count / 2 - 1, cableCountPerGroup / 2)
                .Select(i => Enumerable.Range(i, cableCountPerGroup / 2)
                    .Concat(Enumerable.Range(cableList.Count - 1 - i - (cableCountPerGroup / 2 - 1),
                        cableCountPerGroup / 2))
                    .Select(j => cableList[j].Id).ToList()).ToList();

            //
            // cable unit force load case
            //
            var outputLines = new List<string>
            {
                "*STLDCASE"
            };
            outputLines.AddRange(Enumerable.Range(0, cableGroups.Count)
                .Select(i => $"T{i + 1}, USER,"));

            outputLines.AddRange(cableGroups.SelectMany((g, i) =>
            {
                var groupLines = new List<string>
                {
                    $"*USE-STLD, T{i + 1}",
                    "*PRETENSION"
                };
                groupLines.AddRange(g.Select(id => $"{id}, {pretension},"));
                return groupLines;
            }));

            outputLines.AddRange(new[]
            {
                "*LOADCOMB",
                $"NAME={loadCombName}, GEN, ACTIVE, 0, 0, , 0, 0",
                $"ST, {deadLoadName1}, 1, ST, {deadLoadName2}, 1",
                string.Join(Environment.NewLine, Enumerable.Range(0, cableGroups.Count)
                    .Select(i => $"ST, T{i + 1}, 1"))
            });

            TextBoxCableUnitForce.Text = string.Join(Environment.NewLine, outputLines);

            // 
            // constrain conditions
            // 
            outputLines.Clear();
            outputLines.AddRange(new[]
            {
                "*UNKCONS",
            });
            outputLines.AddRange(constrainDxNodeIds.Select(id =>
                $"DX_N{id}, DISP, {id}, 0, 0, NO, YES, {maxDx}, YES, {minDx}"));
            outputLines.AddRange(constrainDzNodeIds.Select(id =>
                $"DZ_N{id}, DISP, {id}, 0, 2, NO, YES, {maxDz}, YES, {minDz}"));
            outputLines.AddRange(constrainTensionElemIds.Select(id =>
                {
                    var el = _mctModel.AllElemDict[id];
                    var n1 = _mctModel.AllNodeDict[el.NodeIds[0]];
                    var n2 = _mctModel.AllNodeDict[el.NodeIds[1]];
                    var dz = Abs(n1.Z - n2.Z);
                    var tension = ((selfWeight + deadLoad2) * sectionLength / 2) * n1.DistTo(n2) / dz;
                    var maxTension = tension * maxTensionRatio;
                    var minTension = tension * minTensionRatio;
                    return $"T_E{id}, TRUSS, {id}, 1, 0, NO, YES, {maxTension:F0}, YES, {minTension:F0}";
                }
            ));

            outputLines.AddRange(new[]
            {
                "*UNKFACTOR",
                $"NAME=未知索力系数, {loadCombName}, SQUARE, POS, NO"
            });
            outputLines.Add(string.Join(", ",
                constrainDxNodeIds.Select(id => $"DX_N{id}")
                    .Concat(constrainDzNodeIds.Select(id => $"DZ_N{id}"))
                    .Concat(constrainTensionElemIds.Select(id => $"T_E{id}"))));
            outputLines.Add(string.Join(", ", Enumerable.Range(0, cableGroups.Count)
                .Select(i => $"T{i + 1}, 1")));

            TextBoxUnknownFactor.Text = string.Join(Environment.NewLine, outputLines);
        }

        private void ButtonCopyCableUnitForceMct_OnClick(object sender, RoutedEventArgs e)
        {
            var text = TextBoxCableUnitForce.Text;
            if (string.IsNullOrWhiteSpace(text))
                return;

            Clipboard.SetData(DataFormats.Text, text);
        }

        private void ButtonSaveCableUnitForceMct_OnClick(object sender, RoutedEventArgs e)
        {
            var text = TextBoxCableUnitForce.Text;
            if (string.IsNullOrWhiteSpace(text))
                return;

            var sfd = new SaveFileDialog() {FileName = "斜拉索单位荷载", DefaultExt = "mct"};
            if (sfd.ShowDialog(this) != true)
                return;

            var filePath = sfd.FileName;
            File.WriteAllText(filePath, text, Encoding.GetEncoding("gb2312"));
        }

        private void ButtonCopyUnknownFactorMct_OnClick(object sender, RoutedEventArgs e)
        {
            var text = TextBoxUnknownFactor.Text;
            if (string.IsNullOrWhiteSpace(text))
                return;

            Clipboard.SetData(DataFormats.Text, text);
        }

        private void ButtonSaveUnknownFactorMct_OnClick(object sender, RoutedEventArgs e)
        {
            var text = TextBoxUnknownFactor.Text;
            if (string.IsNullOrWhiteSpace(text))
                return;

            var sfd = new SaveFileDialog() {FileName = "未知荷载系数", DefaultExt = "mct"};
            if (sfd.ShowDialog(this) != true)
                return;

            var filePath = sfd.FileName;
            File.WriteAllText(filePath, text, Encoding.GetEncoding("gb2312"));
        }
    }

    public class MctModel
    {
        public Dictionary<int, Node> AllNodeDict { get; private set; }
        public Dictionary<int, Elem> AllElemDict { get; private set; }
        public Dictionary<string, Group> GroupDict { get; private set; }

        public void ReadMctFileData(string mctFilePath)
        {
            Debug.Assert(!string.IsNullOrWhiteSpace(mctFilePath) &&
                         File.Exists(mctFilePath));

            var allLines = File.ReadAllLines(mctFilePath, Encoding.GetEncoding("gb2312"));
            Debug.Assert(allLines != null && allLines.Length > 0);

            var keywordBlocks = new List<KeywordBlock>();
            for (var i = 0; i < allLines.Length; i++)
            {
                var line = allLines[i].Trim();
                if (!line.StartsWith("*"))
                    continue;

                var tokens = line.LeftOf(";").Split(',');
                var keywordBlock = new KeywordBlock
                {
                    Keyword = tokens[0].Trim(),
                    Options = new List<string>(tokens.Skip(1)), // Skip the keyword token
                    DataLines = new List<string>()
                };

                while (i + 1 < allLines.Length)
                {
                    var nextLine = allLines[i + 1].Trim();
                    if (nextLine.StartsWith("*")) // next keyword
                        break;

                    i++;
                    if (string.IsNullOrWhiteSpace(nextLine) || nextLine.StartsWith(";"))
                        continue;

                    keywordBlock.DataLines.Add(nextLine);
                }

                keywordBlocks.Add(keywordBlock);
            }

            AllNodeDict = keywordBlocks.First(kb => kb.Keyword == "*NODE").DataLines
                .Select(Node.FromDataLine)
                .ToDictionary(node => node.Id, node => node);
            AllElemDict = keywordBlocks.First(kb => kb.Keyword == "*ELEMENT").DataLines
                .Select(Elem.FromDataLine)
                .ToDictionary(elem => elem.Id, elem => elem);
            GroupDict = Group.ConcatDataLines(keywordBlocks.First(kb => kb.Keyword == "*GROUP").DataLines)
                .Select(Group.FromDataLine)
                .ToDictionary(group => group.Name, group => group);
        }
    }

    public class KeywordBlock
    {
        public string Keyword { get; set; }
        public List<string> Options { get; set; }
        public List<string> DataLines { get; set; }
    }

    public class Node
    {
        public int Id { get; set; }

        public double X { get; set; }
        public double Y { get; set; }
        public double Z { get; set; }

        public double DistTo(Node other)
            => Sqrt(Pow(X - other.X, 2) + Pow(Y - other.Y, 2) + Pow(Z - other.Z, 2));

        public static Node FromDataLine(string dataLine)
        {
            var tokens = dataLine.Split(',');
            return new Node
            {
                Id = int.Parse(tokens[0].Trim()),
                X = double.Parse(tokens[1].Trim()),
                Y = double.Parse(tokens[2].Trim()),
                Z = double.Parse(tokens[3].Trim()),
            };
        }
    }

    public class Elem
    {
        public int Id { get; set; }
        public List<int> NodeIds { get; set; }

        public static Elem FromDataLine(string dataLine)
        {
            var tokens = dataLine.Split(',');
            return new Elem
            {
                Id = int.Parse(tokens[0].Trim()),
                NodeIds = new List<int>
                {
                    int.Parse(tokens[4].Trim()),
                    int.Parse(tokens[5].Trim()),
                }
            };
        }
    }

    public class Group
    {
        public string Name { get; set; }
        public List<int> NodeIds { get; set; }
        public List<int> ElemIds { get; set; }

        public string NodeIdsExpression { get; set; }
        public string ElemIdsExpression { get; set; }

        public static Group FromDataLine(string dataLine)
        {
            var tokens = dataLine.Split(',');
            var nodeIdsExpression = tokens[1].Trim();
            var elemIdsExpression = tokens[2].Trim();
            return new Group
            {
                Name = tokens[0].Trim(),
                NodeIds = IdsHelper.ParseIdsDataLine(nodeIdsExpression),
                ElemIds = IdsHelper.ParseIdsDataLine(elemIdsExpression),
                NodeIdsExpression = nodeIdsExpression,
                ElemIdsExpression = elemIdsExpression,
            };
        }

        public static List<string> ConcatDataLines(IList<string> dataLines)
        {
            var newDataLines = new List<string>();
            var sb = new StringBuilder();
            foreach (var dataLine in dataLines)
            {
                if (dataLine.EndsWith("\\"))
                {
                    sb.Append(dataLine.Left(dataLine.Length - 1));
                    continue;
                }

                sb.Append(dataLine);
                newDataLines.Add(sb.ToString());
                sb.Clear();
            }

            return newDataLines;
        }
    }

    public static class IdsHelper
    {
        private const string Pattern = @"^([1-9]\d*)(?:(?:to?([1-9]\d*)by([1-9]\d*))|(?:to?([1-9]\d*)))?$";
        private static readonly Regex Regex = new Regex(Pattern, RegexOptions.IgnoreCase);

        private static (int IdFrom, int IdTo, int IncrBy) ParseIdsExpression(string idsExpression)
        {
            Debug.Assert(!string.IsNullOrWhiteSpace(idsExpression));
            var idFrom = 0;
            var idTo = 0;
            var incrBy = 1;

            var match = Regex.Match(idsExpression);
            Debug.Assert(match.Success);
            if (!match.Success)
                return (idFrom, idTo, incrBy);

            var groups = match.Groups;
            var v1 = groups[1].Value;
            Debug.Assert(groups.Count == 5 && !string.IsNullOrWhiteSpace(v1));
            idFrom = int.Parse(v1);
            idTo = idFrom;
            Debug.Assert(idFrom > 0);

            var v2 = groups[2].Value;
            var v4 = groups[4].Value;
            if (!string.IsNullOrWhiteSpace(v4)) // with only "to"
            {
                idTo = int.Parse(v4);
                Debug.Assert(idTo >= idFrom);
            }
            else if (!string.IsNullOrWhiteSpace(v2)) // with "to" and "by"
            {
                idTo = int.Parse(v2);
                Debug.Assert(idTo >= idFrom);

                var v3 = groups[3].Value;
                Debug.Assert(!string.IsNullOrWhiteSpace(v3));
                incrBy = int.Parse(v3);
                Debug.Assert(incrBy > 0);
            }

            return (idFrom, idTo, incrBy);
        }

        public static List<int> ParseIdsDataLine(string idsDataLine)
        {
            var tokens = idsDataLine.Split(new[] {' ', ','}, StringSplitOptions.RemoveEmptyEntries)
                .Select(s => s.Trim());
            var ids = new List<int>();
            foreach (var token in tokens)
            {
                var (idFrom, idTo, incrBy) = ParseIdsExpression(token);
                if (idFrom <= 0)
                    continue;

                Debug.Assert(idTo >= idFrom && incrBy > 0);
                var id = idFrom;
                do
                {
                    ids.Add(id);
                    id += incrBy;
                } while (id <= idTo);
            }

            return ids;
        }
    }
}